#
# Perforce.py
#
# Main SNiFF interface for SNiFF+ 3.x Perforce Adapter
#
# Author: Martin Oberhuber
#         with original contributions by Luc Tanguay, Bell Canada
# Copyright (c) 1998, 1999 TakeFive Software, Salzburg
#
# Revisions:
AdapterVersion = "Perforce Adapter %VERSION%, Perforce.py $Revision: 1.12 $"

import os, string, regsub, time, traceback
import vcs
from vcs import TEST, TRUE, FALSE, NL, SHELL, SYSTEM, WINDOWS, UNIX
from vcs import NULLDEV, DELIMITER
#from plog import Plog
import P4, P4Prefs, P4Dialogs, P4Log
from P4Prefs import P4Error
from P4Log import P4log
from P4 import detect

# ---- log handling ----------------------------------

_last_work_file		= ''
_last_work_dir		= ''
_last_log		= None
## log cache. If workingFile is a Depot file (//...), it will directly access the depot.
def log_with_history(workingDir, workingFile, use_cache=FALSE):
	global _last_work_file, _last_work_dir, _last_log
	if use_cache and (_last_work_file == workingFile) and (_last_work_dir == workingDir):
		if P4Prefs.TEST: print "(Perforce) log_hist --> re-use ",workingFile
		_last_log.reuse()
		return _last_log
	else:
		if P4Prefs.TEST: print "(Perforce) log_hist (",workingDir, ", ", workingFile, ")"
		theRoot=P4.GetRoot(vcs.privateRoot, vcs.SSWE, workingDir)
		p4 = P4.P4Iter('-d %s where %s' % (workingDir, workingFile), vcs.repositoryRoot, theRoot)
		depotFiles=""
		for curLine in p4.lines:
			try:
				depotFiles = depotFiles+string.split(curLine)[0] + " "
			except:
				pass
		if string.find(workingFile, ' ') > 0:	# multiple files #BUGBUG spaceInFileName
			# find the correct client for the workingDir
			#return P4log(workingDir, workingFile)
			return P4log(workingDir, depotFiles, workingFile)
		_last_work_file = workingFile
		_last_work_dir = workingDir
		#_last_log = P4log(workingDir, workingFile)
		_last_log = P4log(workingDir, depotFiles, workingFile)
		_last_log.close()
		if not _last_log:
			_last_work_file = ''
		return _last_log


def getRevision():
	if P4Prefs.TEST:
		print "revision  : ", vcs.revision
		print "revision2 : ", vcs.revision2
		print "symbolName: ",vcs.symbolName
	if vcs.revision:
		if vcs.revision[:2]=="//": return P4Log.UnCodeRev(vcs.revision)
		return '#' + vcs.revision
	elif vcs.revision2:
		if vcs.revision2[:2]=="//": return P4Log.UnCodeRev(vcs.revision2)
		return '#' + vcs.revision2
	rev = vcs.symbolName
	##COPIED from RCS: SNiFF-internal HEAD_xxx:yyy -- not used by Perforce
	#if rev[:4] in ('HEAD', 'INIT', '') or string.find(rev,':') >= 0:
	#	rev = vcs.revision
	if rev[:4] == 'HEAD':
		return ''
	elif rev[:4] == 'INIT':
		return '#1'
	return rev


def addRevision(workFile,rev):
	if rev[:2] == '//':
		# revision specifier as direct depot path
		return rev
	return workFile + rev


def NotImplemented(theFunction=""):
	P4Dialogs.ErrorDialog("Sorry, but the function @B%s@B is not implemented." % theFunction)


# ---- VCS commands ----------------------------------

def CheckOut():
	marker = 'Perforce.CheckOut:'

	# handling of exclusive locking is configurable in Preferences.
	lockType = vcs.lockType
	if lockType == 'E':
		if P4Prefs.ConcurrentLockOnCheckoutE:
			print marker, 'Concurrent Locking is preferred at your site. Exclusive lock ignored.'
			lockType = 'C'
		elif not P4Prefs.AllowExclusiveLocking:
			print marker, 'Exclusive Locking is disabled at your site. Please use concurrent lock instead.'
			return FALSE
		# end if
	# end if

	# In Perforce, Revisions are passed by symbolName and just appended to the workingFile.
	# We always work with repositoryFile, which is a relative path from the PWE.
	workingFile = addRevision(vcs.repositoryFile, getRevision())
	if P4Prefs.TEST: print marker,'workingFile is %s' % workingFile

	if vcs.workingDir == vcs.tmpDir:
		# temporary Checkout for differing versions against each other
		theFile = vcs.joinpath(vcs.workingDir, vcs.workingFile)
		p4 = P4.P4Iter('-d %s print -q %s > %s ; chmod ugo-w %s' % (vcs.privateRoot, workingFile, theFile, theFile))
		return TRUE

	# Regular Checkout
	p4 = P4.P4Iter('-d %s sync -f %s' % (vcs.privateRoot,workingFile))
	try:
		res = p4[0]
	except:
		traceback.print_exc()
		return FALSE
	# end try

	# Check the results
	x = detect(res,['- added as','up-to-date','- refreshing','- updating'])
	if not x:
		print marker,'error syncing %s' % workingFile
		print marker,'error:',res
		return FALSE
	# end if

	P4.InvalidateLockersCache(vcs.joinpath(vcs.privateRoot, vcs.repositoryFile))
	if lockType == 'N': return TRUE

	# Concurrent lock is requested... since the sync has already been done,
	# we should not use the version number here... anyway, Perforce allows us 
	# to use it here, too.
	# don't use the version spec here
	p4 = P4.P4Iter('-d %s edit %s' % (vcs.privateRoot,vcs.repositoryFile))
	try:
		res = p4[0]
	except:
		traceback.print_exc()
		return FALSE
	# Check the results.
	x = detect(res,['- opened for edit','currently opened for edit'])
	if not x:
		print marker,'error editing %s' % workingFile
		print marker,'error:',res
		# end if
		return FALSE
	# end if

	if lockType == 'E': return Lock(1)
	return TRUE
# end def CheckOut


def CheckOutProject():
	marker = 'Perforce.CheckOutProject:'
	p4 = P4Iter( '-d %s' % vcs.workingDir, 'sync', '...')
	firstLine = p4[0]
	# If p4 returns nothing, it means something went wrong...
	if not firstLine:
		print marker, 'no lines from p4 output...'
		return FALSE
	# end if
	return TRUE
# end def CheckOutProject


def CreateRepositoryFile():
	return CheckIn()
# end def CreateRepositoryFile


def CheckIn():
	#print "File-based CheckIn is not possible with Perforce."
	#print "Please use the @BSubmit@B Command from the @BPerforce@B menu instead."
	#return FALSE
	msg="""File-based checkin via the File->CheckIn menu is not possible with Perforce because
the tool has a workspace-oriented concept. Please use the @BPerforce@B menu with

	- @BPerforce -> Create/Edit Client Specification@B to create an initial mapping for Perforce
	- @BPerforce -> Add files...@B to initially add files to the depot, and
	- @BPerforce -> Submit...@B to finally check in your modifications."""
	P4Dialogs.ErrorDialog(msg)
	return TRUE
# end def CheckIn


def Lock(isEdited=0):
	if not P4Prefs.AllowExclusiveLocking:
		P4Dialogs.ErrorDialog("Exclusive Locking is disabled at your site. Nothing done.")
		return TRUE
	# Locking can only happen on the PWE, therefore we need not find the
	# real client of the file.
	if not isEdited:
		# First opening the file for edit can't harm if it's already edited.
		p4 = P4.P4Iter('-d %s edit %s' % (vcs.privateRoot, vcs.repositoryFile))
	p4 = P4.P4Iter('-d %s lock %s' % (vcs.privateRoot, vcs.repositoryFile))
	for curLine in p4.lines:
		if string.find(curLine,"- locking") < 0:
			print "p4 lock error:", curLine
	P4.InvalidateLockersCache(vcs.joinpath(vcs.privateRoot, vcs.repositoryFile))
	return TRUE
# end def Lock


def SetWorkFileReadOnly():
	return TRUE
# end def SetWorkFileReadOnly


def UpdateProject():
	return TRUE
# end def UpdateProject


_dont_touch_private_files = os.environ.has_key("SNiFF_DONT_TOUCH_PRIVATE_FILES")
def UpdateFile():
	# first of all, simply sync the workfile -- that's what we've been asked for!
	# According to the clobber option that the user has set, writable files might
	# be synced too! But we will never sync -f (so we won't overwrite 
	# existing changes).

	workfile = vcs.joinpath(vcs.workingDir, vcs.workingFile)
	# Attach version number (e.g. changelist number) to file
	repoFile = "%s%s" % (vcs.repositoryFile, getRevision())
	# first let's see whether the workfile already exists.
	wf_existed=0
	forceOpt="-f"
	wf_stat = vcs.get_stat(workfile)
	if vcs.test_exists(wf_stat):
		if vcs.SSWS:
			if os.path.islink(workfile):
				wf_existed=1
				forceOpt=""
	p4 = P4.P4Iter("-d %s sync %s %s" % (vcs.privateRoot, forceOpt, repoFile))
	try:
		res = p4[0]
	except:
		import traceback
		traceback.print_exc()
	# end try
	x = detect(res,['- added as','up-to-date','updating', 'refreshing'])
	if not x:
		vcs.output("UpdateFile: error syncing @B%s@B: %s" % (workfile, res))

	# the workfile is now up-to-date, perhaps it has been clobbered.
	wf_stat = vcs.get_stat(workfile)
	if vcs.test_writable(wf_stat):
		if vcs.SSWS:
			if os.path.islink(workfile):
				return TRUE
			if _dont_touch_private_files:
				return TRUE
			vcs.output('Touching private writable file @B%s@B' % vcs.workingFile)
			vcs.touch(workfile)
		return TRUE

	# workfile is not writable
	if SYSTEM==UNIX:
		if vcs.test_mode(wf_stat, 0022):
			vcs.output('Warning: Private file @B%s@B writable by other user' % vcs.workingFile)
			return TRUE

	if not vcs.SSWS:
		# project not shared: we are finished.
		return TRUE

	# project is shared
	if vcs.TEST: print 'Project is shared'
	SSWSlist = string.split(vcs.SSWS, DELIMITER)

	# file exists in local WS: we know that since we just sync'd the file
	if vcs.TEST: print 'Private file exists'
	shared_file = ''
	for SSWS in SSWSlist:
		shared_file = vcs.joinpath(SSWS, vcs.catDir, vcs.workingFile)
		sf_stat = vcs.get_stat(shared_file)
		if vcs.test_exists(sf_stat):
			if SYSTEM==WINDOWS or not os.path.islink(shared_file):
				break
		shared_file = ''
	if not shared_file:
		# shared file does not exist: we are done because it's already synced
		if vcs.TEST: print 'Shared file does not exist'
		return TRUE

	# shared file exists
	if vcs.TEST: print 'Shared file:', shared_file
	rc = vcs.system('diff %s %s > %s' % (workfile, shared_file, NULLDEV))
	if rc == 1:
		# shared file is not up-to-date
		if vcs.TEST: print 'Shared file is not up-to-date'
		if not wf_existed:
			# the workfile didn't exist; we synced it before!
			vcs.output('--> Checking out @B%s@B' % vcs.workingFile)
			return TRUE
		if _dont_touch_private_files:
			return TRUE
		vcs.output('Touching private read-only file @B%s@B' % vcs.workingFile)
		vcs.touch(workfile)
		return TRUE
	elif rc == 2:
		return FALSE

	#shared file is up-to-date - remove local
	if vcs.TEST: print 'Shared file is up-to-date'
	if os.path.islink(workfile):
		return TRUE
	##better sync#none instead of rm_f ;-)
	#rc = vcs.rm_f(workfile)
	#if rc == 0:
	p4 = P4.P4Iter("-d %s sync %s#none" % (vcs.privateRoot, vcs.repositoryFile))
	try:
		res = p4[0]
	except:
		traceback.print_exc()
	x = detect(res,['- deleted as'])
	if not x:
		vcs.output("UpdateFile: error deleting @B%s@B: %s" % (vcs.workingFile, res))
		return FALSE
	else:
		# if the workfile didn't exist before our sync it was just synced and 
		# removed again -- no need to tell the user ;-)
		if wf_existed:
			vcs.output('Private file @B%s@B is deleted, shared file is used' % vcs.workingFile)
	return TRUE


def Unlock():
	if not P4Prefs.AllowExclusiveLocking:
		P4Dialogs.ErrorDialog("Exclusive Locking is disabled at your site. Nothing done.")
		return TRUE
	# Locking can only happen on the PWE, therefore we need not find the
	# real client of the file.
	p4 = P4.P4Iter('-d %s unlock %s' % (vcs.privateRoot, vcs.repositoryFile))
	for curLine in p4.lines:
		if string.find(curLine,"- unlocking") < 0:
			print "p4 unlock error:", curLine
	P4.InvalidateLockersCache(vcs.joinpath(vcs.privateRoot, vcs.repositoryFile))
	return TRUE
# end def Unlock


def BreakLock():
	return Unlock()
# end def 

def Comment():
	log = log_with_history(vcs.workingDir, vcs.workingFile, TRUE)
	if not log:
		return FALSE
	rev = log.revision(vcs.revision)
	if not rev:
		return FALSE
	vcs.output(rev.log_message)
	return TRUE


def DeleteVersion():
	## could be implemented by p4 obliterate
	NotImplemented("Delete Version")
	return TRUE
# end def DeleteVersion


def SetDescription():
	NotImplemented("Change Description")
	return TRUE
# end def SetDescription


def SetSymbol():
	NotImplemented("Set Symbol")
	return TRUE
# end def SetSymbol


def DeleteSymbol():
	NotImplemented("Delete Symbol")
	return TRUE
# end def DeleteSymbol


def ChangeComment():
	NotImplemented("Change Comment")
	return TRUE
# end def ChangeComment

def Description():
	return TRUE


def History():
	log = log_with_history(vcs.workingDir, vcs.workingFile, TRUE)
	if not log:
		return FALSE
	vcs.output(log.history_str())
	return TRUE
# end def History

def Head():
	log = log_without_history(vcs.privateRoot, vcs.repositoryFile, TRUE)
	if not log:
		return FALSE
	vcs.output(log.head_rev())
	return TRUE

def SymbolList():
	marker = 'Perforce.SymbolList:'
	if P4Prefs.TEST: print marker, vcs.workingDir, vcs.workingFile
	labels = []
	try:
		# BUGBUG Disable labels since it takes VERY long....
		# labels = P4.labels()
		# will need repFiles only if labels are wanted and no exception
		# when setting labels
		repFiles = string.split(vcs.repositoryFile)
	except:
		import sys
		x = sys.exc_info()
		print marker,'%s caught, value "%s", trace: %s' % (x[0],x[1],x[2])
	# vcs.repositoryFile is always the full path to the file
	log = log_with_history(vcs.workingDir, vcs.workingFile, FALSE)
	if not log:
		return FALSE
	out_list = []
	while log:
		out_list.append('\014%s' % log.work_file())
		out_list.append('HEAD: %s' % log.head_rev())
		symbols = log.symbolstr()
		if symbols:
			out_list.append(symbols)
		# Perforce: labels wanted too?
		for label in labels:
			if P4Prefs.TEST: print marker,'check if "%s" in label "%s"' % (repFiles[i],label)
			revision = P4.revisionForLabel(repFiles[i],label)
			if revision > 0:
				if P4Prefs.TEST: print marker,'yes, revision is %d' % revision
				out_list.append('%s: %d\n' % (label,revision))
			# end if
		# end for
		for rev in log.revision_list():
			theText = rev.p4rev + " " + rev.rev + " "
			theText = theText + rev.date + " 00:00:01 ; " + rev.author + " ; " # + rev.lines +  ";"
			out_list.append(theText)
		if log.is_last_file():
			break
		else:
			log.next()
	theOutput = string.join(out_list, NL)
	if P4Prefs.TEST: print theOutput
	vcs.output(theOutput)
	return TRUE
# end def SymbolList

def RevisionOfSymbol():
	return FALSE
# end def RevisionOfSymbol

def HistoryOfRevision():
	if P4Prefs.TEST: print "revision  : ",vcs.revision
	if P4Prefs.TEST: print "revision2 : ",vcs.revision2
	if P4Prefs.TEST: print "symbolName: ",vcs.symbolName
	log = log_with_history(vcs.workingDir, vcs.workingFile, TRUE)
	if not log:
		return FALSE
	rev = log.revision(vcs.revision)
	if not rev:
		print "(HistoryOfRevision) Error: Not found revision: vcs.revision = ",vcs.revision
		return FALSE
	vcs.output(str(rev))
	return TRUE
# end def HistoryOfRevision

def Lockers():
	marker = 'Perforce.Lockers:'
	if P4Prefs.TEST: print marker
	out_list = []
	# take the repositoryFile so we don't get relative paths due to fileType directories
	repFiles=string.split(vcs.repositoryFile)
	workFiles=string.split(vcs.workingFile)
	for i in xrange(len(workFiles)):
		theRoot=P4.GetRoot(vcs.privateRoot, vcs.SSWE, repFiles[i])
		lockerString = P4.LockerString(vcs.repositoryRoot, theRoot, repFiles[i], vcs.user)
		if lockerString:
			out_list.append(workFiles[i]+" "+lockerString)
		else:
			out_list.append(workFiles[i])
	vcs.output(string.join(out_list, NL))
	return TRUE

def IsLockedBy():
	return FALSE
# end def IsLockedBy

def IsLocked():
	return FALSE
# end def IsLocked

def OriginalIsDifferent():
	theRev = getRevision()
	try:
		theRoot=P4.GetRoot(vcs.privateRoot, vcs.SSWE, vcs.repositoryFile, vcs.workingDir)
		p4 = P4.P4Iter('-d %s diff %s%s' % (vcs.workingDir,vcs.workingFile, theRev), vcs.repositoryRoot, theRoot)
		res = string.join(p4.lines[1:],'\n')
	except:
		res = ''
	# note: must return TRUE if they are NOT DIFFERENT!
	return res == ''
# end def OriginalIsDifferent

def IsDifferent():
	# in Perforce, the IsDifferent is best used to show those files
	# which are out-of-sync, i.e. they are candidates for resolve.
	# this is most simply done by using the lockers column and showing
	# those files which have a "have" revision different than the "head"
	# revision. This should be much faster than complete comparison.
	try:
		theRoot=P4.GetRoot(vcs.privateRoot, vcs.SSWE, vcs.repositoryFile)
		curDict = P4.fstat(vcs.repositoryRoot, theRoot, vcs.repositoryFile)
		headRev=curDict["headRev"]
		haveRev=curDict["haveRev"]
		if headRev == haveRev:
			#print "IsDifferent: Revisions match!"
			# files should be taken as "unmodified" unless they are writable and different.
			st=vcs.get_stat(vcs.joinpath(vcs.workingDir, vcs.workingFile))
			if not vcs.test_writable(st):
				# file is regarded as unmodified
				return TRUE
			#print "IsDifferent: Writable"
			theRoot=P4.GetRoot(vcs.privateRoot, vcs.SSWE, vcs.repositoryFile, vcs.workingDir)
			# just compare against the version that it was synced from!
			p4 = P4.P4Iter('-d %s diff %s' % (vcs.workingDir,vcs.workingFile), vcs.repositoryRoot, theRoot)
			res = string.join(p4.lines[1:],'\n')
			#print "IsDifferent: res=|%s|" % res
			return res==''
	except:
		#print "IsDifferent Exception:"
		#import traceback; traceback.print_exc()
		pass
	# new files, files not on client etc. are all regarded as "not modified".
	return FALSE


def DiffText_W_V():
	marker = 'Perforce.DiffText_W_V:'
	theRev = getRevision()
	try:
		theRoot=P4.GetRoot(vcs.privateRoot, vcs.SSWE, vcs.repositoryFile, vcs.workingDir)
		p4 = P4.P4Iter('-d %s print -q %s > %s' % (vcs.workingDir, addRevision(vcs.workingFile, theRev), vcs.tmpFile), vcs.repositoryRoot, theRoot)
		res = string.join(p4.lines,'\n')
	except:
		res = ''
		import traceback
		traceback.print_exc()
	# end try
	args2 = (vcs.joinpath(vcs.workingDir, vcs.workingFile), vcs.tmpFile)
	out = vcs.getoutput('diff -b %s %s' % args2)
	vcs.output(out)
	return out == ''
# end def DiffText_W_V


def DiffText_V_V():
	p4 = P4.P4Iter('-d %s where %s' % (vcs.privateRoot, vcs.repositoryFile), vcs.repositoryRoot, vcs.privateRoot)
	try:
		depotFile = string.split(p4.lines[0])[0]
	except:
		vcs.output("Error finding RepositoryFile: "+p4.lines[0])
		return FALSE
	rev1=getRevision()
	vcs.revision=vcs.revision2
	rev2=getRevision()
	p4=P4.P4Iter("diff2 %s %s" % (addRevision(depotFile, rev1), addRevision(depotFile, rev2)))
	if string.find(p4.lines[0], "==== identical") > 0:
		return TRUE
	try:
		res = string.join(p4.lines[1:], vcs.NL)
		vcs.output(res)
	except:
		vcs.output("Error in diff2: "+p4.lines[0])
		traceback.print_exc()
	return FALSE
# end def DiffText_V_V

# The following are also set in Perforce.sniff; what about fileType directory?
def RepositoryDir():
	vcs.output(vcs.joinpath(vcs.projectDir))
	return TRUE

def RepositoryFile():
	if vcs.repositoryDir:
		vcs.output(vcs.joinpath(vcs.repositoryDir, vcs.workingFile))
	else:
		vcs.output(vcs.workingFile)
	return TRUE

def TempDir():
	vcs.output(vcs.joinpath(vcs.OSTempDir,'.sniffdir2','Perforce_%s%s' % (vcs.user, vcs.pid)))
	return TRUE

def TempFile():
	vcs.output(vcs.joinpath(vcs.tmpDir, vcs.workingFile))
	return TRUE

def BranchDef():
	vcs.output('X')
	return TRUE

def RepositoryCheckDef():
	vcs.output('E')
	return TRUE

# CheckRepositoryDir is set in Perforce.sniff
def CheckRepositoryDir():
	return TRUE

def CheckRepositoryFile():
	if P4Prefs.TEST: print "CheckRepositoryFile: PWE=",vcs.privateRoot,"; workDir=",vcs.workingDir,"; workFile=",vcs.workingFile,"; repoFile=",vcs.repositoryFile
	theRoot = P4.GetRoot(vcs.privateRoot, vcs.SSWE, vcs.repositoryFile, vcs.workingDir)
	theStat=P4.fstat(vcs.repositoryRoot, theRoot, vcs.repositoryFile)
	if theStat:
		return TRUE
	#return FALSE
	# also show files for which no client is found
	return TRUE
# end def CheckRepositoryFile

def DateOfRepositoryFile():
	# don't make an extra p4 call to retrieve the *real* head time - this call is only
	# used to check whether caches are still valid, so just always return current time.
	t = time.time()
	lt = time.localtime(t)
	# Return with format YYY/MM/DD HH:MM:SS.
	vcs.output(time.strftime('%Y/%m/%d %H:%M:%S',lt))
	return TRUE
# end def DateOfRepositoryFile


def CreateRepositoryDir():
	return TRUE
# end def CreateRepositoryDir


def TestTool():
	if not vcs.is_reachable('p4'):
		P4Dialogs.ErrorDialog("Perforce: @Bp4@B Client Executable not found. Please check your path!")
		return FALSE
	return TRUE
# end def TestTool

def Constructor():
	if not os.path.isdir(vcs.tmpDir):
		vcs.mkdir_p(vcs.tmpDir)
	#If lockers are on, this may lead to Window Sync problems, so don't print it
	#print AdapterVersion
	return TRUE


def Destructor():
	if os.path.isdir(vcs.tmpDir):
		vcs.rm_fr(vcs.tmpDir)
	return TRUE

# eof Perforce.py

